// Level Classes.
// ==============

// The tileSet class.
class pp_tileSet {
  int tileRows, tileColumns, tileWidth, tileHeight, firstGID;
  PImage tileSet;
  
  pp_tileSet (int rows, int cols, int tWidth, int tHeight, int gid, String filename) {
    
    // Test if file exists.
    if (pp_fileExists(filename)) {
      
      // Load the sprite.
      tileSet = loadImage(filename);
      tileRows = rows;
      tileColumns = cols;
      tileWidth = tWidth;
      tileHeight = tHeight;
      firstGID = gid;
      
    // Else, print error.
    } else {
      println("Image " + dataPath(filename) + " not found.");
    }
  }
}

// The level class.
class pp_level {
  pp_tileSet[] tileSetList;
  String tileSetRef;
  HashMap<String, pp_script> scripts;
  int levelWidth, levelHeight, levelColumns, levelRows, levelLayers, backgroundLayers, tileWidth, tileHeight;
  int[][] levelMap, background;
  float[] bgScroll;
  XML[] objects;
  
  pp_level(String filename){
    
    // Test if file exists.
    if (pp_fileExists(filename)) {
      
      // Load file as XML.
      XML file = loadXML(filename);
      
      // Get tile dimensions.
      tileWidth = file.getInt("tilewidth");
      tileHeight = file.getInt("tileheight");
      
      // Get Row and Column count.
      levelColumns = file.getInt("width");
      levelRows = file.getInt("height");
      
      // Get level dimensions.
      levelWidth = levelColumns * tileWidth;
      levelHeight = levelRows * tileHeight;
      
      // Process tileSets.
      XML[] tileSets = file.getChildren("tileset");
      tileSetRef = "";
      tileSetList = new pp_tileSet[ tileSets.length ];  // Resize tileSetList according to tileSet count.      
      
      for (int i = 0; i < tileSets.length; i++) {
        
        // Load each tileSet.
        XML data = tileSets[i];
        
        // Get first GID.
        int gid = data.getInt("firstgid");
        
        // Load tileSet image info.
        XML image = data.getChild("image");
        
        // Extract tileSet filename.
        String[] nameParts = split(image.getString("source"), "/");
        String imageFileName = "Tilesets/" + nameParts[nameParts.length - 1];
        
        // Get tile count.
        int tileColumns = image.getInt("width") / tileWidth;
        int tileRows = image.getInt("height") / tileHeight;
        
        // Create new tileSet.
        tileSetList[i] = new pp_tileSet(tileRows, tileColumns, tileWidth, tileHeight, gid, imageFileName);
        
        // Append to tileRef.
        for (int k = 0; k < tileRows * tileColumns; k++){
          tileSetRef += nf(i,2);
        }
      }
      
      // Get objects.
      objects = file.getChildren("objectgroup");
      
      // Get scripts.
      scripts = new HashMap<String, pp_script>();
      for (XML objectType : objects) {
        String objectName = objectType.getString("name");
    
        // Check type of object.
        if (objectName.substring(0,1).equals("(")) {
          
          // Break up name into parts.
          String[] nameParts = split(objectName, " ");
          
          // If script.
          if (nameParts[0].equals("(script)")) {
        
            // Grab script name.
            String name = nameParts[1];
        
            // Grab coords array (if any).
            int[] scriptCoords;
            String coords = objectType.getChild("object").getChild("polyline").getString("points");
            if (coords != null) {
              String[] coordsArray = split(coords, " ");
              
              // Get X and Y coords at pt "0".
              int startX = int(objectType.getChild("object").getString("x"));
              int startY = int(objectType.getChild("object").getString("y"));
              
              // Grab coords array and convert them to level coords.
              scriptCoords = new int[coordsArray.length];
              for (int i = 0; i < coordsArray.length; i++) {
                String[] coordsPair = split(coordsArray[i], ",");
                scriptCoords[i] = pp_coordToArray(int(coordsPair[0]) + startX, int(coordsPair[1]) + startY, this);
              }
            } else {
              scriptCoords = null;
            }
        
            // Get the total amount of frames.
            int endFrame = 0; 
            for (XML obj : objectType.getChild("properties").getChildren("property")) {    
              String frame = split(obj.getString("name"), " ")[0];
              endFrame = max(endFrame, int(frame));
            }
        
            // Create the scriptAction container "children".
            HashMap<String, pp_scriptAction[]> children = new HashMap<String, pp_scriptAction[]> ();
        
            // Create the pp_script object.
            pp_script Script = new pp_script(endFrame, scriptCoords, children);
            scripts.put(name, Script);
        
            // Create scriptAction temp container. This one uses ArrayList so we can dynamically add scriptActions to it.
            HashMap<String, ArrayList<pp_scriptAction>> scriptTemp = new HashMap<String, ArrayList<pp_scriptAction>>();
            
            // Add scriptActions to the temp container.
            for (XML obj : objectType.getChild("properties").getChildren("property")) {
              String frame = split(obj.getString("name"), " ")[0];
              
              // Grab scriptAction function.
              String[] actionArray = split(obj.getString("value"), "(");
              String params = actionArray[1].substring(0, actionArray[1].length() - 1);
              pp_scriptAction action = null;
              try {action = (pp_scriptAction) Class.forName(app.getClass().getName() + "$script_" + actionArray[0]).getDeclaredConstructors()[0].newInstance(new Object[]{app, params, Script});}
              catch (Exception e) {println(e);}
          
              // Check whether there is an entry in the scriptTemp array.
              if (scriptTemp.get(frame) != null) {
                scriptTemp.get(frame).add(action);
              } else {
                // Initiate Arraylist.
                scriptTemp.put(frame, new ArrayList<pp_scriptAction>());
                scriptTemp.get(frame).add(action);
              }
            }
            
            // Turn over contents of temp container to "children" array.
            for (Map.Entry entry : scriptTemp.entrySet()) {
              pp_scriptAction[] tempActions = new pp_scriptAction[ scriptTemp.get(entry.getKey()).size() ];
              for (int i = 0; i < scriptTemp.get(entry.getKey()).size(); i++) {
                tempActions[i] = scriptTemp.get(entry.getKey()).get(i);
              }
              children.put(entry.getKey().toString(), tempActions);
            }
          }
        }
      }
      
      // Load level layers.
      XML[] layers = file.getChildren("layer");
      
      // Count number of background and level layers.
      for (int i = 0; i < layers.length; i++) {
        if (layers[i].getString("name").substring(0,4).equals("(bg)")) {backgroundLayers++;}
        else {levelLayers++;}
      }
      
      // Initiate arrays.
      background = new int[backgroundLayers][levelColumns * levelRows];
      levelMap = new int[levelLayers][levelColumns * levelRows];
      bgScroll = new float[backgroundLayers];
      
      // Process each layer.
      int currentLayer = 0;
      int bgCurrentLayer = 0;
      for (int i = 0; i < layers.length; i++) {
        
        // If it's a background or foreground layer...
        if (layers[i].getString("name").substring(0,4).equals("(bg)")) {
          
          // Get properties.
          HashMap<String, String> prop = new HashMap<String, String>();
          for (XML property : layers[i].getChild("properties").getChildren("property")) {
            prop.put(property.getString("name"), property.getString("value"));
          }
          
          // Process the scroll rate.
          bgScroll[bgCurrentLayer] = constrain(float(getProp(prop, "scroll")), 0, 1);
          
          // Process each background tile.
          XML[] tiles = layers[i].getChild("data").getChildren("tile");
          for (int k = 0; k < tiles.length; k++) {
            background[bgCurrentLayer][k] = tiles[k].getInt("gid");
          }
          
          // Increase background layer counter.
          bgCurrentLayer++;
        }
        
        // Else, load layer as normal.
        else {
          // Load each tile.
          XML[] tiles = layers[i].getChild("data").getChildren("tile");
          for (int k = 0; k < tiles.length; k++) {
            levelMap[currentLayer][k] = tiles[k].getInt("gid");
          }
          currentLayer++;
        }
      }
    
    // Else, print error.
    } else {
      println("Level " + dataPath(filename) + " not found.");
    }
  }
}

// Level Functions.
// ================

void pp_loadLevel(String levelName) {
  
  // Clear the current level.
  pp_clearAllObjects();
  
  // Get the level file.
  pp_level level = lvl.get(levelName);
  
  // Set the room dimensions.
  pp_roomWidth = level.levelWidth;
  pp_roomHeight = level.levelHeight;
  
  // Load room objects.
  for (XML objectType : level.objects) {
    String objectName = objectType.getString("name");
    
    // Check type of object.
    if (objectName.substring(0,1).equals("(")) {
        
      // Break up name into parts.
      String[] nameParts = split(objectName, " ");
        
      // If levelEffect controller.
      if (nameParts[0].equals("(levelEffect)")) {
          
        // Grab layer properties.
        HashMap<String, String> prop = new HashMap<String, String>();
        for (XML property : objectType.getChild("properties").getChildren("property")) {
          prop.put(property.getString("name"), property.getString("value"));
        }
          
        // Process properties and create levelEffect controller.
        pp_emitters.add(
          new pp_levelEffects (
            int(getProp(prop, "spawnEdge")),
            int(getProp(prop, "amt")),
            int(getProp(prop, "rate")),
            int(getProp(prop, "depth")),
            float(getProp(prop, "scroll")),
            getProp(prop, "xFormula"),
            getProp(prop, "yFormula"),
            getProp(prop, "sprite")
          )
        );
      }
      
      // If Decal.
      if (nameParts[0].equals("(decal)")) {
        
        // Grab layer properties.
        HashMap<String, String> prop = new HashMap<String, String>();
        for (XML property : objectType.getChild("properties").getChildren("property")) {
          prop.put(property.getString("name"), property.getString("value"));
        }
        
        // Set layer properties.
        int layerDepth = int(getProp(prop, "depth"));
        int layerOrigin = int(getProp(prop, "origin"));
        String layerSprite = getProp(prop, "sprite");
        
        // Process each object in decal layer.
        for (XML object : objectType.getChildren("object")) {
          
          // Grab local properties if any.
          HashMap<String, String> localProp = new HashMap<String, String>();
          if (object.hasChildren()) {
            for (XML property : object.getChild("properties").getChildren("property")) {
              localProp.put(property.getString("name"), property.getString("value"));
            }
          }
          
          int myDepth = max(layerDepth, int(getProp(localProp, "depth")));
          int myOrigin = max(layerOrigin, int(getProp(localProp, "origin")));
          String mySprite = "";
          if (getProp(localProp, "sprite").equals("")) {
           mySprite = layerSprite;
          } else {
           mySprite = getProp(localProp, "sprite");
          }
          
          // If decal is a tile.
          if (mySprite.equals("")){
            pp_decals.add(
              new pp_decal(
                object.getInt("x"),
                object.getInt("y"),
                myDepth,
                object.getInt("gid")
              )
            );
            
          // If decal is a sprite.
          } else {
            pp_decals.add(
              new pp_decal(
                object.getInt("x"),
                object.getInt("y"),
                myDepth,
                myOrigin,
                mySprite
              )
            );
          }
        }
      }                
      
    // Else, create contained objects as pp_gameObjects.
    } else {    
      XML[] objects = objectType.getChildren("object");
      for (XML object : objects) {
        
        // Create temp object using Java reflection.
        Object obj = null;
        String ClassName = app.getClass().getName() + "$" + objectName;
        try {obj = Class.forName(ClassName).getDeclaredConstructors()[0].newInstance(new Object[]{this});}
        catch (Exception e) {println(e);}
      
        if (obj != null) {
        
          // Set paramenters and create object as pp_gameObject.
          pp_gameObject gameObject = (pp_gameObject)obj;
          gameObject.setX ( object.getInt("x") );
          gameObject.setY ( object.getInt("y") );
          gameObject.setWidth ( object.getInt("width") );
          gameObject.setHeight ( object.getInt("height") );
          gameObject.setType ( objectName );
          gameObject.init();
      
          // Add object to pp_objects array.
          pp_objects.add(gameObject);
        }
      }
    }
  }
  
  // Set current level.
  pp_currentLevel = level;
}

void pp_updateLevel() {
  if (pp_currentLevel != null){
    int renderTileStartX, renderTileStartY, renderTileEndX, renderTileEndY;
    
    // Process each background layer.
    for (int h = 0; h < pp_currentLevel.backgroundLayers; h++) {
      // Get scroll rate for this layer.
      float bgScrollRate = pp_currentLevel.bgScroll[h];
      
      // Gather some offset'ed dimensions.
      int bgDisplayX = int((((1 - bgScrollRate) / 2) * pp_currentLevel.levelWidth) + (pp_displayX * bgScrollRate));
      int bgDisplayY = int((((1 - bgScrollRate) / 2) * pp_currentLevel.levelHeight) + (pp_displayY * bgScrollRate));
    
      // Determine how much of the background is seen.
      renderTileStartX = int(bgDisplayX / pp_currentLevel.tileWidth);
      renderTileStartY = int(bgDisplayY / pp_currentLevel.tileHeight);
      renderTileEndX = ceil((bgDisplayX + (width / pp_pixelSize)) / pp_currentLevel.tileWidth);
      renderTileEndY = ceil((bgDisplayY + (height / pp_pixelSize)) / pp_currentLevel.tileHeight);
    
      // Render visible background tiles.
      // Process each background tile.
      for (int i = renderTileStartX; i <= renderTileEndX; i++) {
        for (int k = renderTileStartY; k <= renderTileEndY; k++) {
          
          // Get GID of tile.
          int gid = pp_currentLevel.background[h][i + (k * pp_currentLevel.levelColumns)];
          
          // Draw tile in game level.
          if (gid != 0) {
            pp_drawTile(
              (i * pp_currentLevel.tileWidth) - bgDisplayX + pp_displayX,
              (k * pp_currentLevel.tileHeight) - bgDisplayY + pp_displayY,
              gid,
              pp_currentLevel.tileSetList[int(pp_currentLevel.tileSetRef.substring((gid - 1) * 2, gid * 2))]
            );
          }
        }
      }
    }
    
    // Determine how much of the level is seen.
    renderTileStartX = max(0, int(pp_displayX / pp_currentLevel.tileWidth));
    renderTileStartY = max(0, int(pp_displayY / pp_currentLevel.tileHeight));
    renderTileEndX = min(pp_currentLevel.levelColumns, ceil((pp_displayX + (width/pp_pixelSize)) / pp_currentLevel.tileWidth));
    renderTileEndY = min(pp_currentLevel.levelRows, ceil((pp_displayY + (height/pp_pixelSize)) / pp_currentLevel.tileHeight));
    
    // Render visible tiles.
    // Process each layer.
    for (int h = 0; h < pp_currentLevel.levelLayers; h++) {
      
      // Process each tile.
      for (int i = renderTileStartX; i <= renderTileEndX; i++) {
        for (int k = renderTileStartY; k <= renderTileEndY; k++) {
          
          // Get GID of tile.
          int gid = pp_currentLevel.levelMap[h][i + (k * pp_currentLevel.levelColumns)];
          
          // Draw tile in game level.
          if (gid != 0) {
            pp_drawTile(
              i * pp_currentLevel.tileWidth,
              k * pp_currentLevel.tileHeight,
              gid,
              pp_currentLevel.tileSetList[int(pp_currentLevel.tileSetRef.substring((gid - 1) * 2, gid * 2))]
            );
          }
        }
      }
    }
  }
}

void pp_drawTile(int x, int y, int gid, pp_tileSet tileSet) {
  
  // Get tile coord on tileSet image.
  gid -= tileSet.firstGID;
  int tileX = (gid % tileSet.tileColumns) * tileSet.tileWidth;
  int tileY = int(gid / tileSet.tileColumns) * tileSet.tileHeight;
  
  // Test if tile is on-screen.
  int testResult = 
    pp_toInt(pp_testOnScreen(x, y)) + 
    pp_toInt(pp_testOnScreen(x + tileSet.tileWidth, y)) +
    pp_toInt(pp_testOnScreen(x, y + tileSet.tileHeight)) +
    pp_toInt(pp_testOnScreen(x + tileSet.tileWidth, y + tileSet.tileHeight));
    
  // If partially on screen...
  if (testResult < 4) {
    // Get margins.
    int startX = (x < pp_displayX) ? (pp_displayX - x) : 0;
    int endX = (x + tileSet.tileWidth >= int(width/pp_pixelSize) + pp_displayX) ? ((int(width/pp_pixelSize) + pp_displayX) - x) : tileSet.tileWidth;
    int startY = (y < pp_displayY) ? (pp_displayY - y) : 0;
    int endY = (y + tileSet.tileHeight >= int(height/pp_pixelSize) + pp_displayY) ? ((int(height/pp_pixelSize) + pp_displayY) - y) : tileSet.tileHeight;
      
    // Render partially to screen.
    for (int i = startX; i < endX; i++) {
      for (int k = startY; k < endY; k++) {
        pp_drawPixel(i + x, k + y, tileSet.tileSet.pixels[i + tileX + ((tileY + k) * tileSet.tileSet.width)]);
      }
    }
    
  // Else, render all.
  } else {
    for (int i = 0; i < tileSet.tileWidth; i++) {
      for (int k = 0; k < tileSet.tileHeight; k++) {
        pp_drawPixel(i + x, k + y, tileSet.tileSet.pixels[i + tileX + ((tileY + k) * tileSet.tileSet.width)]);
      }
    }
  }
}

String getProp (HashMap<String, String> prop, String index) {
  String result = prop.get(index);
  return (result == null) ? "" : result ;
}
